using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace BouncingBall
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        private GraphicsDeviceManager graphicsDeviceManager;
        private SpriteBatch spriteBatch;
        private TimeSpan elapsedUpdateTime;
        private bool isKeyEscapeDown;
        private bool isKeyTDown;
        private BasicEffect basicEffect;
        private Plane groundPlane;
        private Model ballModel;
        private bool shouldUseGifTexture;
        private Texture2D ballTexture;
        private Vector3 ballVelocity;
        private Vector3 ballGravity;
        private Matrix ballTranslations;
        private bool shouldDrawBoundingBox;
        private IndexBuffer indexBuffer;
        private Color[] ballBoundingBoxColors;

        public Game1()
        {
            this.graphicsDeviceManager = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            IsMouseVisible = true;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // Create the basiceffect to define the view on the world and objects
            basicEffect = new BasicEffect( GraphicsDevice, null );
            basicEffect.World = Matrix.Identity;
            basicEffect.View = Matrix.CreateLookAt(new Vector3(0, 5.5f, 15), new Vector3(0, 5.5f, 0), Vector3.Up);
            basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView( MathHelper.ToRadians( 45 ),
                                                                          (float) Window.ClientBounds.Width / (float) Window.ClientBounds.Height,
                                                                          0.1f,
                                                                          100f );

            // Define the ground plane, and ball velocity. Also set the first translation for the ball to the identity matrix
            groundPlane = new Plane( Vector3.Up, 0 );
            ballVelocity = Vector3.Up;
            ballTranslations = Matrix.CreateTranslation(ballVelocity);

            // Create an indexbuffer for the boundingbox we might have to draw
            short[] indices = new short[] { 0, 2, 1, // Front
                                            0, 3, 2,
                                            4, 7, 3, // Left
                                            0, 3, 4,
                                            1, 2, 6, // Right
                                            5, 1, 6,
                                            0, 1, 5, // Top
                                            4, 0, 5,
                                            3, 7, 2, // Bottom
                                            6, 7, 2,
                                            7, 4, 5, // Back
                                            5, 7, 6 };
            indexBuffer = new IndexBuffer(GraphicsDevice, sizeof(short) * indices.Length, BufferUsage.None, IndexElementSize.SixteenBits);
            indexBuffer.SetData<short>( indices );

            // Make the random colors for the models boundingbox
            Random random = new Random();
            ballBoundingBoxColors = new Color[8];
            for (int i = 0; i < ballBoundingBoxColors.Length; i++)
            {
                ballBoundingBoxColors[i] = new Color( (float) random.NextDouble(),
                                                      (float) random.NextDouble(),
                                                      (float) random.NextDouble() );
            }

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            this.ballModel = Content.Load<Model>( "ballGravity" );
            ballGravity = (Vector3) ballModel.Meshes[0].Tag;
            
            ballTexture = Content.Load<Texture2D>("ballTexture");
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            #region Timing
            elapsedUpdateTime += gameTime.ElapsedGameTime;
            bool shouldUpdate = false;

            if (elapsedUpdateTime.Milliseconds > 30)
            {
                elapsedUpdateTime = TimeSpan.Zero;
                shouldUpdate = true;
            }
            #endregion

            #region Key Events
            if (Keyboard.GetState().IsKeyUp(Keys.Escape) && isKeyEscapeDown)
            {
                Exit();
            }
            if (Keyboard.GetState().IsKeyUp(Keys.T) && isKeyTDown)
            {
                shouldUseGifTexture = !shouldUseGifTexture;
            }

            if (shouldUpdate)
            {
                if ( Keyboard.GetState().IsKeyDown( Keys.Left ) )
                {
                    ballVelocity += new Vector3( -0.005f, 0, 0 );
                }
                if ( Keyboard.GetState().IsKeyDown( Keys.Right ) )
                {
                    ballVelocity += new Vector3(0.005f, 0, 0);
                }
                if ( Keyboard.GetState().IsKeyDown( Keys.Up ) )
                {
                    ballVelocity += new Vector3(0, 0, -0.005f);
                }
                if ( Keyboard.GetState().IsKeyDown( Keys.Down ) )
                {
                    ballVelocity += new Vector3(0, 0, 0.005f);
                }
            }

            isKeyEscapeDown = Keyboard.GetState().IsKeyDown(Keys.Escape);
            isKeyTDown = Keyboard.GetState().IsKeyDown(Keys.T);
            #endregion

            #region Mouse Events
            Ray mouseRay = new Ray();

            if (shouldUpdate)
            {
                // Define the near and far mouse positions in parallel planes
                Vector3 mousePositionNear = new Vector3( Mouse.GetState().X, Mouse.GetState().Y, 0 );
                Vector3 mousePositionFar = new Vector3( Mouse.GetState().X, Mouse.GetState().Y, 1 );

                // Unproject both positions to align them with the viewing frustrum
                Vector3 unprojectedMousePositionNear = GraphicsDevice.Viewport.Unproject( mousePositionNear,
                                                                                          basicEffect.Projection,
                                                                                          basicEffect.View,
                                                                                          Matrix.Identity );
                Vector3 unprojectedMousePositionFar = GraphicsDevice.Viewport.Unproject( mousePositionFar,
                                                                                         basicEffect.Projection,
                                                                                         basicEffect.View,
                                                                                         Matrix.Identity );

                // By subtracting the near position from the far position, we get a direction. We normalize it to make it
                // a direction, in stead of a full blown vector
                Vector3 direction = unprojectedMousePositionFar - unprojectedMousePositionNear;
                direction.Normalize();

                // Make the ray by moving from the unprojected near position, to the direction
                mouseRay = new Ray( unprojectedMousePositionNear, direction );
            }
            #endregion

            #region Ball Control
            if (shouldUpdate)
            {
                // Determine the boundingbox of the model using the current transformations. Due to the custom processor, the boundingbox
                // is stored in the Tag when processed
                BoundingBox ballBoundingBox = ((BoundingBox)ballModel.Tag);

                // Get the corners of the bounding box
                Vector3[] boundingBoxCorners = ballBoundingBox.GetCorners();

                // For every corner, translate it to the right position
                for (int i = 0; i < boundingBoxCorners.Length; i++)
                {
                    boundingBoxCorners[i] = Vector3.Transform(boundingBoxCorners[i], ballTranslations);
                }
                ballBoundingBox = BoundingBox.CreateFromPoints( boundingBoxCorners );

                // If our mouse hovers above the model, we should draw its boundingbox
                shouldDrawBoundingBox = ballBoundingBox.Intersects( mouseRay ) != null;

                // If our model collides with the ground (a predefined plane at 0, 0, 0, with a normal of 0, 1, 0)
                if ( ballBoundingBox.Intersects( groundPlane ) ==
                     PlaneIntersectionType.Intersecting )
                {
                    // Reflect off the plane (in case we are moving sideways and/or in the z direction)
                    ballVelocity = Vector3.Reflect( ballVelocity, groundPlane.Normal );
                }
                // This else causes the model to bounce forever. If we add the gravity before this if, we can make it go higher every step.
                // If we add the gravity after this if, the model will bounce lower and lower until it stops.
                else
                {
                    // Apply the gravity to the velocity of the model
                    ballVelocity += ballGravity;
                }

                // Apply the current velocity of the model to the translations we already calculated (this will move the model 1 step)
                ballTranslations *= Matrix.CreateTranslation( ballVelocity );
            }
            #endregion

            #region Ball Colors
            if (shouldUpdate)
            {
                // For every color, change it a bit and save it again
                for (int i = 0; i < ballBoundingBoxColors.Length; i++)
                {
                    // Get the color as a vector (Color.R, Color.G and Color.B are bytes)
                    Vector3 tempColor = ballBoundingBoxColors[i].ToVector3();
                    // Add a small value to R, G and B values
                    tempColor.X += 0.01f;
                    tempColor.Y += 0.01f;
                    tempColor.Z += 0.01f;

                    // If any of the values are above 1, reset it to 0
                    if (tempColor.X > 1)
                    {
                        tempColor.X = 0;
                    }
                    if (tempColor.Y > 1)
                    {
                        tempColor.Y = 0;
                    }
                    if (tempColor.Z > 1)
                    {
                        tempColor.Z = 0;
                    }

                    // Put the new color back in the array
                    ballBoundingBoxColors[i] = new Color( tempColor.X, tempColor.Y, tempColor.Z );
                }
            }
            #endregion

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // Always draw the model in a solid manner
            GraphicsDevice.RenderState.FillMode = FillMode.Solid;
            GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;

            basicEffect.World *= Matrix.CreateRotationY( MathHelper.ToRadians( 0.5f ) );

            // Draw the model
            foreach (ModelMesh mesh in this.ballModel.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.World = basicEffect.World * ballTranslations;
                    effect.View = basicEffect.View;
                    effect.Projection = basicEffect.Projection;

                    // Set some default lighting and possibly enable the .gif texture
                    effect.EnableDefaultLighting();
                    effect.TextureEnabled = true;

                    if (shouldUseGifTexture)
                    {
                        effect.Texture = ballTexture;
                    }
                    else
                    {
                        effect.Texture = null;
                    }
                }

                mesh.Draw();
            }

            if (shouldDrawBoundingBox)
            {
                // Get the bounding box, and transform it as the model has been transformed
                BoundingBox ballBoundingBox = ( (BoundingBox) ballModel.Tag );

                // Get the corners of the bounding box and make a vertex position color array
                Vector3[] vertexVectors = ballBoundingBox.GetCorners();
                VertexPositionColor[] vertices = new VertexPositionColor[vertexVectors.Length];

                // For every position vector, make a vertex position color out of it
                for (int i = 0; i < vertexVectors.Length; i++)
                {
                    vertices[i] = new VertexPositionColor( vertexVectors[i],
                                                           ballBoundingBoxColors[i] );
                }

                // Make the vertexbuffer with the vertices we just made
                VertexBuffer vertexBuffer = new VertexBuffer( GraphicsDevice, VertexPositionColor.SizeInBytes * vertices.Length, BufferUsage.None );
                vertexBuffer.SetData<VertexPositionColor>( vertices );

                // Apparently, drawing proper colored vertices, with lighting or not, won't work if we don't set the vertex declaration every time
                // we draw. Maybe the model messes up the vertex declaration and indexbuffer?
                GraphicsDevice.VertexDeclaration = new VertexDeclaration( GraphicsDevice, VertexPositionColor.VertexElements );

                // Set the vertexbuffer in a vertex stream of the graphics device
                GraphicsDevice.Vertices[0].SetSource( vertexBuffer, 0, VertexPositionColor.SizeInBytes );
                
                // Apparently, drawing a proper cube won't work if we don't set the index buffer after the vertex stream
                GraphicsDevice.Indices = indexBuffer;
                
                // Set the boundingbox to display as a wireframe, and discard the winding order of the primitives
                GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
                GraphicsDevice.RenderState.CullMode = CullMode.None;

                // Save the world matrix, so we don't lose it after manipulating it while drawing the boundingbox
                Matrix tempWorld = basicEffect.World;

                // Begin the drawing, set the vertex colors to display and enable some default lighting
                basicEffect.Begin();

                // With this we manipulate the boundingbox to match the draw model
                basicEffect.World *= ballTranslations;

                // Enable the vertex colors
                basicEffect.VertexColorEnabled = true;

                // Draw all the primitives as a trianglelist
                foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
                {
                    pass.Begin();
                    GraphicsDevice.DrawIndexedPrimitives( PrimitiveType.TriangleList, 0, 0, vertices.Length, 0, 12 );
                    pass.End();
                }

                // End the drawing
                basicEffect.End();

                // Set the world matrix back
                basicEffect.World = tempWorld;
            }

            base.Draw(gameTime);
        }
    }
}